package com.sijobe.installer;

import java.io.File;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

/**
 * Handles the install event
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author simo_415
 */
public class ThreadInstall extends Thread {

   private List files;
   private File destination;
   private Method callback;
   private ManifestHandler manifest;
   private File installBase;
   private String project;

   public ThreadInstall(String project, List installFiles, File installBase, File destination, ManifestHandler manifest) {
      this.files = installFiles;
      this.destination = destination;
      this.manifest = manifest;
      this.installBase = installBase;
      this.project = project;
   }

   /**
    * Sets the call back method when the installation has completed 
    * 
    * @param callback - The static method to call back
    */
   public void setCallback(Method callback) {
      this.callback = callback;
   }

   /**
    * Signals the call back method when the installation has completed
    * 
    * @param err - The message to send the call back method
    */
   private void signalDone(String err) {
      if (callback != null) {
         try {
            callback.invoke(null, new Object[]{err});
         } catch (Exception e) {}
      }
   }

   /**
    * Installs the specified files
    * 
    * @param project
    * @param files
    * @param installBase
    * @param destination
    * @param manifest
    * @return
    */
   public static String install(String project, List files, File installBase, File destination, ManifestHandler manifest) {
      if (manifest == null) {
         manifest = new ManifestHandler(Util.getFile("manifest.xml",files));
         if (manifest.getProjectNames().length == 0) {
            return "Couldn't find manifest file.";
         }
      }

      String filenames[] = new String[files.size()];
      int i = 0;
      for (i = 0; i < files.size(); i++) {
         File file = (File)files.get(i);
         filenames[i++] = file.getAbsolutePath();
      }

      // Gets the files to install from the manifest and pushes them into a HashMap 
      // with the key being the destination and the value being a Vector containing
      // all the files to put into the destination
      HashMap destinationh = new HashMap();
      //Vector v = (Vector)((HashMap)manifest.get("Single Player Commands")).get("manifest");
      List v = manifest.getProjectManifest(project);

      for (i = 0; i < v.size(); i++) {
         Object temp = v.get(i);
         ManifestFile t = null;
         if (temp instanceof ManifestFile) {
            t = (ManifestFile)temp;
         } else if (temp instanceof ManifestScript) {
            if (!(executeScript((ManifestScript)temp,files,installBase,destination))) {
               System.out.println("ERROR " + ((ManifestScript)temp).getSource() + " failed to run.");
            }
            continue;
         } else {
            continue; // Shouldn't get here
         }
         // Get the destination
         String dest = new File(destination,t.getDestination()).getAbsolutePath();
         // Get the source and replace . (regex "all") with \\. and * with .*
         String src = (t.getSource()).replaceAll("\\.", "\\.").replaceAll("\\*", ".*");
         src = "\\Q" + installBase.getAbsolutePath().replaceAll("\\\\","/") + "\\E" + "/" + src;
         Vector source = new Vector();
         System.out.println("DEBUG " + dest + " " + src);

         // Loop through files in installation and match them using the manifest to 
         // the correct destination
         for (int j = 0; j < files.size(); j++) {
            File f = (File)files.get(j);
            if (f.getAbsolutePath().replaceAll("\\\\","/").matches(src)) {
               source.add(f);
            } else if (f.isDirectory()) {
               if ((f.getAbsolutePath().replaceAll("\\\\","/") + "/").matches(src)) {
                  source.add(f);
               }
            }
         }

         if (!source.isEmpty()) {
            Vector o = (Vector)destinationh.get(dest);
            if (o == null) {
               destinationh.put(dest, source);
            } else {
               o.addAll(source);
               destinationh.put(dest, o);
            }
         }
      }

      // Installs the files
      Iterator it = destinationh.keySet().iterator();
      while (it.hasNext()) {
         String key = (String)it.next();
         System.out.println("DEBUG " + key);
         File destpath = new File(key);
         if (!destpath.exists()) {
            continue;
         }
         File tmp = null;
         boolean recompress = false;
         // Extracts the file if destination is a file
         if (destpath.isFile()) {
            tmp = Util.getTempDirectory();
            if (!Util.extractArchive(destpath,tmp)) {
               // TODO: Add to a failed list?
               System.out.println("ERROR - Failed installing: " + destpath.getAbsolutePath() + " > " + tmp.getAbsolutePath());
               continue;
            }
            recompress = true;
         } else if (destpath.isDirectory()) {
            tmp = new File(key);
         } else {
            continue;
         }

         // Copies source files to destination
         Vector d = (Vector)destinationh.get(key);
         for (int j = 0; j < d.size(); j++) {
            File destfile = (File)d.elementAt(j);
            if (destfile.isFile()) {
               Util.copyFile(destfile,new File(tmp,destfile.getName()));
            } else if (destfile.isDirectory()) {
               Util.copyDirectory(destfile,new File(tmp,destfile.getName()));
            }
         }

         // Extra rules specifically for Minecraft.jar - TODO: delete needs to be added to manifest
         if (destpath.isFile() && destpath.getName().equalsIgnoreCase("minecraft.jar")) {
            Util.delete(new File(tmp,"META-INF/CODESIGN.SF"));
            Util.delete(new File(tmp,"META-INF/CODESIGN.rsa"));
         }

         // Recompress the archive if needed
         if (recompress) {
            if (!Util.compressFiles(tmp,destpath)) {
               return "Unable to install to: " + destpath.getAbsolutePath() +
               "\n\nA common cause for this is that the file is already open elsewhere.";
            }
         }

         // Delete the temporary location if created
         if (tmp.exists() && destpath.isFile()) {
            Util.delete(tmp);
         }
      }
      return null;
   }

   /**
    * Executes the provided script
    * 
    * @param script - The script to execute
    * @param files - The files contained in the archive (one of which is the script)
    * @param source - Where the installation files are located
    * @param destination - Where the destination directory is located
    */
   public static boolean executeScript(ManifestScript script, List files, File source, File destination) {
      if (script.getSource() == null || script.getType() == null) {
         return false;
      }
      File scriptfile = Util.getFile(script.getSource(), files);
      if (scriptfile == null || !scriptfile.exists()) {
         return false;
      }
      
      try {
         // Loads any dependencies onto the classpath
         if (script.getDependencies() != null) {
            for (int i = 0; i < script.getDependencies().length; i++) {
               File dependency = Util.getFile(script.getDependencies()[i], files);
               if (!Util.addToClasspath(dependency)) {
                  System.out.println("Unable to add specified dependency " + dependency.getName());
                  return false;
               }
            }
         }
         
         // Checks if the script type is java based
         if (script.getType().equalsIgnoreCase("java")) {
            if (!Util.addToClasspath(scriptfile)) {
               return false;
            }
            Class loader = Class.forName(script.getClassname());
            Method main = loader.getMethod("main",new Class[] {String[].class});
            main.setAccessible(true);
            if (!main.isAccessible()) {
               return false;
            }
            main.invoke(null, new Object[] { new String[] { 
                     script.getClassname(), 
                     source.getAbsolutePath(), 
                     destination.getAbsolutePath() 
            }});
         }
      } catch (Exception e) {
         e.printStackTrace();
         return false;
      }
      return true;
   }

   /**
    * Executes the installation
    * 
    * @see java.lang.Thread#run()
    */
   public void run() {
      signalDone(install(project,files,installBase,destination,manifest));
   }
}
